<?php

/**
 * Dispatcher class.
 *
 *
 * @package    lib
 * @subpackage lib_dispatcher
 * @author     Juan Carlos Jarquin (zetta)
 * @author     $LastChangedBy$
 * @version    1.1, SVN: $Id$
 */
class Dispatcher
{
  /**
   * @var string
   */
  private $controllerPath = 'controllers';
  
  /**
   * @var string
   */
  private $viewPath = 'views';
  
  /**
   * @var string
   */
  private $defaultController = 'index';
  
  /**
   * @var string
   */
  private $errorController = 'error';
  
  /**
   * @var string
   */
  private $defaultAction = 'index';
  
  /**
   * @var AbstractController
   */
  private $controller = null;
  
  /**
   * Request Object
   * @var Request
   */
  private $request = null;
  
  /**
   * Response Object
   * @var Response
   */
  private $response = null;
  
  /**
   * Router Object
   * @var Router
   */
  private $router = null;
  
  /**
   * @var View
   */
  private $view = null;
  
  /**
   * Path delimiter character
   * @var string
   */
  protected $pathDelimiter = '_';
  
  /**
   * Array of invocation parameters to use when instantiating action
   * controllers
   * @var array
   */
  protected $invokeParams = array();
  
  /**
   * Class Constructor
   *
   * @return ControllerManager
   */
  public function Dispatcher()
  {
    $this->request = Request::getInstance();
    $this->response = Response::getInstance();
    $this->router = Router::getInstance();
  }
  
  /**
   * Word delimiter characters
   * @var array
   */
  protected $wordDelimiter = array('-', '.');
  
  /**
   * Dispatch the controller
   */
  public function dispatch()
  {
    
    $this->view = View::getInstance($this->viewPath,'cache/twig');
    
    if (! is_dir($this->controllerPath))
      throw new DispatcherException($this->controllerPath . ' path does not exists');
    $this->router->assemble($this->request);
    
    /**
     * Get controller class
     */
    if (! $this->isDispatchable($this->request))
    {
      $controller = $this->request->getControllerName();
      if (! $this->getParam('useDefaultControllerAlways') && ! empty($controller))
      {
        throw new DispatcherException('Invalid controller specified (' . $this->request->getControllerName() . ')');
      }
      $className = $this->getDefaultControllerClass($this->request);
    }
    else
    {
      $className = $this->getControllerClass($this->request);
      if (! $className)
      {
        $className = $this->getDefaultControllerClass($this->request);
      }
    }
    
    if (! ProjectAutoloader::getInstance()->hasClass($className, true))
      throw new DispatcherException(" Controller {$className} does not exists ");
    
    $reflectionClass = new ReflectionClass($className);
    $this->controller = $reflectionClass->newInstance();
    
    if (! ($this->controller instanceof AbstractController))
      throw new DispatcherException("Controller \"{$className}\" is not an instance of AbstractController");
    
    /**
     * Retrieve the action name
     */
    $action = $this->getActionMethod($this->request);
    
    // by default, buffer output
    $disableOb = $this->getParam('disableOutputBuffering');
    $obLevel = ob_get_level();
    if (empty($disableOb))
    {
      ob_start();
    }
    
    try
    {
      $this->controller->prepare();
      $this->controller->preDispatch();
      $this->controller->dispatch($action);
      $this->controller->postDispatch();
    } 
      catch ( Exception $e )
    {
      // Clean output buffer on error
      $curObLevel = ob_get_level();
      if ($curObLevel > $obLevel)
      {
        do
        {
          ob_get_clean();
          $curObLevel = ob_get_level();
        } while ( $curObLevel > $obLevel );
      }
      
      throw $e;
    }
    
    if (empty($disableOb))
    {
      $content = ob_get_clean();
      $this->response->appendBody($content);
    }
    
    // Destroy the page controller instance and reflection objects
    $controller = null;
    
    $this->response->sendResponse();
  
  }
  
  /**
   * @param string
   */
  public function setControllerPath($controllerPath)
  {
    $this->controllerPath = $controllerPath;
  }
  
  /**
   * Determine the action name
   *
   * First attempt to retrieve from request; then from request params
   * using action key; default to default action
   *
   * Returns formatted action name
   *
   * @param Request $request
   * @return string
   */
  public function getActionMethod(Request $request)
  {
    $action = $request->getActionName();
    if (empty($action))
    {
      $action = $this->getDefaultAction();
      $request->setActionName($action);
    }
    
    return $this->formatActionName($action);
  }
  
  /**
   * Retrieve action controller instantiation parameters
   *
   * @return array
   */
  public function getParams()
  {
    return $this->invokeParams;
  }
  
  /**
   * Returns TRUE if the Zend_Controller_Request_Abstract object can be
   * dispatched to a controller.
   *
   * Use this method wisely. By default, the dispatcher will fall back to the
   * default controller (either in the module specified or the global default)
   * if a given controller does not exist. This method returning false does
   * not necessarily indicate the dispatcher will not still dispatch the call.
   *
   * @param Request $action
   * @return boolean
   */
  public function isDispatchable(Request $request)
  {
    $className = $this->getControllerClass($request);
    if (! $className)
    {
      return false;
    }
    
    if (class_exists($className, false))
    {
      return true;
    }
    
    return ProjectAutoloader::getInstance()->hasClass($className, true);
  }
  
  /**
   * Get controller class name
   *
   * Try request first; if not found, try pulling from request parameter;
   * if still not found, fallback to default
   *
   * @param Request $request
   * @return string|false Returns class name on success
   */
  public function getControllerClass(Request $request)
  {
    $controllerName = $request->getControllerName();
    if (empty($controllerName))
    {
      if (! $this->getParam('useDefaultControllerAlways'))
      {
        return false;
      }
      $controllerName = $this->getDefaultControllerName();
      $request->setControllerName($controllerName);
    }
    $className = $this->formatControllerName($controllerName);
    return $className;
  }
  
  /**
   * Retrieve default controller class
   *
   * Determines whether the default controller to use lies within the
   * requested module, or if the global default should be used.
   *
   * By default, will only use the module default unless that controller does
   * not exist; if this is the case, it falls back to the default controller
   * in the default module.
   *
   * @param Request $request
   * @return string
   */
  public function getDefaultControllerClass(Request $request)
  {
    $controller = $this->getDefaultControllerName();
    $default = $this->formatControllerName($controller);
    $request->setControllerName($controller)->setActionName(null);
    
    return $default;
  }
  
  /**
   * Formats a string into an action name.  This is used to take a raw
   * action name, such as one that would be stored inside a Zend_Controller_Request_Abstract
   * object, and reformat into a proper method name that would be found
   * inside a class extending Zend_Controller_Action.
   *
   * @param string $unformatted
   * @return string
   */
  public function formatActionName($unformatted)
  {
    $formatted = $this->formatName($unformatted, true);
    return strtolower(substr($formatted, 0, 1)) . substr($formatted, 1) . 'Action';
  }
  
  /**
   * Retrieve a single parameter from the controller parameter stack
   *
   * @param string $name
   * @return mixed
   */
  public function getParam($name)
  {
    if (isset($this->_invokeParams[$name]))
    {
      return $this->_invokeParams[$name];
    }
    
    return null;
  }
  
  /**
   * Set the default controller (minus any formatting)
   *
   * @param string $controller
   * @return Dispatcher
   */
  public function setDefaultControllerName($controller)
  {
    $this->defaultController = (string) $controller;
    return $this;
  }
  
  /**
   * Retrieve the default controller name (minus formatting)
   *
   * @return string
   */
  public function getDefaultControllerName()
  {
    return $this->defaultController;
  }
  
  /**
   * Set the default action (minus any formatting)
   *
   * @param string $action
   * @return Dispatcher
   */
  public function setDefaultAction($action)
  {
    $this->defaultAction = (string) $action;
    return $this;
  }
  
  /**
   * Retrieve the default action name (minus formatting)
   *
   * @return string
   */
  public function getDefaultAction()
  {
    return $this->defaultAction;
  }
  
  /**
   * Formats a string into a controller name.  This is used to take a raw
   * controller name, such as one stored inside a Zend_Controller_Request_Abstract
   * object, and reformat it to a proper class name that a class extending
   * Zend_Controller_Action would use.
   *
   * @param string $unformatted
   * @return string
   */
  public function formatControllerName($unformatted)
  {
    return ucfirst($this->formatName($unformatted)) . 'Controller';
  }
  
  /**
   * Format the module name.
   *
   * @param string $unformatted
   * @return string
   */
  public function formatModuleName($unformatted)
  {
    if (($this->_defaultModule == $unformatted) && ! $this->getParam('prefixDefaultModule'))
    {
      return $unformatted;
    }
    
    return ucfirst($this->formatName($unformatted));
  }
  
  /**
   * Format action class name
   *
   * @param string $moduleName Name of the current module
   * @param string $className Name of the action class
   * @return string Formatted class name
   */
  public function formatClassName($moduleName, $className)
  {
    return $this->formatModuleName($moduleName) . '_' . $className;
  }
  
  /**
   * Retrieve the path delimiter character(s) used in
   * controller names
   *
   * @return array
   */
  public function getPathDelimiter()
  {
    return $this->pathDelimiter;
  }
  
  /**
   * Set path delimiter
   *
   * Set the path delimiter to use in controllers. May be a single string or
   * an array of strings.
   *
   * @param string $spec
   * @return Zend_Controller_Dispatcher_Abstract
   */
  public function setPathDelimiter($spec)
  {
    if (! is_string($spec))
    {
      throw new Dispatcher('Invalid path delimiter');
    }
    $this->pathDelimiter = $spec;
    
    return $this;
  }
  
  /**
   * Retrieve the word delimiter character(s) used in
   * controller or action names
   *
   * @return array
   */
  public function getWordDelimiter()
  {
    return $this->wordDelimiter;
  }
  
  /**
   * Set word delimiter
   *
   * Set the word delimiter to use in controllers and actions. May be a
   * single string or an array of strings.
   *
   * @param string|array $spec
   * @return Zend_Controller_Dispatcher_Abstract
   */
  public function setWordDelimiter($spec)
  {
    $spec = $this->verifyDelimiter($spec);
    $this->wordDelimiter = $spec;
    
    return $this;
  }
  
  /**
   * Formats a string from a URI into a PHP-friendly name.
   *
   * By default, replaces words separated by the word separator character(s)
   * with camelCaps. If $isAction is false, it also preserves replaces words
   * separated by the path separation character with an underscore, making
   * the following word Title cased. All non-alphanumeric characters are
   * removed.
   *
   * @param string $unformatted
   * @param boolean $isAction Defaults to false
   * @return string
   */
  protected function formatName($unformatted, $isAction = false)
  {
    // preserve directories
    if (! $isAction)
    {
      $segments = explode($this->getPathDelimiter(), $unformatted);
    }
    else
    {
      $segments = (array) $unformatted;
    }
    
    foreach ($segments as $key => $segment)
    {
      $segment = str_replace($this->getWordDelimiter(), ' ', strtolower($segment));
      $segment = preg_replace('/[^a-z0-9 ]/', '', $segment);
      $segments[$key] = str_replace(' ', '', ucwords($segment));
    }
    
    return implode('_', $segments);
  }
  
  /**
   * Verify delimiter
   *
   * Verify a delimiter to use in controllers or actions. May be a single
   * string or an array of strings.
   *
   * @param string|array $spec
   * @return array
   * @throws Zend_Controller_Dispatcher_Exception with invalid delimiters
   */
  public function verifyDelimiter($spec)
  {
    if (is_string($spec))
    {
      return (array) $spec;
    }
    elseif (is_array($spec))
    {
      $allStrings = true;
      foreach ($spec as $delim)
      {
        if (! is_string($delim))
        {
          $allStrings = false;
          break;
        }
      }
      
      if (! $allStrings)
      {
        throw new DispatcherException('Word delimiter array must contain only strings');
      }
      
      return $spec;
    }
    
    throw new DispatcherException('Invalid word delimiter');
  }
  
  /**
   * Convert a class name to a filename
   *
   * @param string $class
   * @return string
   */
  public function classToFilename($class)
  {
    return str_replace('_', DIRECTORY_SEPARATOR, $class) . '.php';
  }
  
  /**
   * Return the value of the currently selected dispatch directory (as set by
   * {@link getController()})
   *
   * @return string
   */
  public function getDispatchDirectory()
  {
    return $this->curDirectory;
  }
  
  /**
   * @return Twig_Environment
   */
  public function getView()
  {
    return $this->view;
  }
  
  /**
   * @param Twig_Environment $view
   */
  public function setView(Twig_Environment $view)
  {
    $this->view = $view;
  }
  
  /**
   * @return string
   */
  public function getViewPath()
  {
    return $this->viewPath;
  }
  
  /**
   * @param string $viewPath
   */
  public function setViewPath($viewPath)
  {
    $this->viewPath = $viewPath;
  }

}


